查看原文
其他

C++ 反射:第三章 原生

CPP开发者 2023-07-27

The following article is from CPP编程客 Author 里缪

第一章:C++ 反射:通识

第二章:C++ 反射:探索


工具能够提升开发效率,却也会限制住思维。

缺少语言机制而实现的反射,就像是停电之时点着蜡烛来取光,虽够用,然多是不便。
原生反射的机制位于原理层,属于底层的、自动的机制,无需用户手动注册任何类型元信息。同时,可以获取类型的Introspection信息比较完整,使用起来非常灵活,在此之上,可以轻松构建许多强大的组件。
上一章,介绍了诸多不同实现的自定义反射库,丰富了开发工具。但是,这些反射库仅属于结构层,反射能力并不完备,取光可以,代替整个反射概念却有不及。
若是仅会使用其中的几个库,对反射在应用层面的思索不免受到桎梏。
因此,本篇介绍一个原生支持静态反射的C++ 编译器:Circle(https://www.circle-lang.org)。
Sean Baxter对C++进行了一系列扩展,增加了许多现代语言所具有的特性,便有了这个新式的C++20编译器。扩展的特性包含dynamic pack property, universal member access, constexpr conditional, imperative arguments等等现代特性,此外,作者也实现了一些C++23提案的扩展,例如pattern matching, if consteval, deducing this。
静态反射,是其中非常重要的一个扩展,是很多扩展的基石。

基本Introspection

上篇当中,展示了诸多T0层反射库的使用例子,其用法难免繁琐,能力也很弱小,与Cicle原生静态反射的便捷程度相去甚远。
举个例子,获取每个结构体的信息,代码如下:
#include <iostream>
#include <string>

struct person {
    int age;
    std::string name;
};


template<typename T>
void print_members() {
    std::cout << @type_string(T) << ":\n";
    @meta for(int i = 0; i < @member_count(T); ++i) {
        std::cout << i << " " << @member_type_string(T, i) 
            << " " << @member_name(T, i) << "\n";
    }
}

int main() {
    print_members<person>();
}
前缀为@的就是扩展的反射工具集,其中@type_string用于得到类型的字符形式,@member_count用于得到类型的成员个数,@member_type_string用于得到成员类型的字符形式,@member_name用于得到成员的名称。
Cicle是静态反射,故所有操作都需要在编译期完成,而普通for发生于运行期。在其前面添加一个@meta关键字,就能使for发生于编译期。
编译输出,将会得到:
$ circle refl.cxx && ./refl
person:
0 int age
1 std::string name
Circle还提供了另外一种更加简便的方式,上述代码还可以这样写:
template<typename T>
void print_members() {
    std::cout << @type_string(T) << ":\n";
    (std::cout << int... << " " << @member_type_strings(T) 
            << @member_names(T) << "\n")...;
}
所有的反射成员工具集都变成了复数形式,用法很像fold expressions。
其中的int...称为pack index,这是在简化标准中的std::index_sequence。
类有权限之分,Circle也支持按照不同的权限来获取类成员,例如:
struct pointer {
publicfloat x;
protectedfloat y;
privatefloat z;
};


int main() {
    //print_members<person>();
    //print_enumerations<Days>();
    (std::cout << int... << ": " << @member_decl_strings(pointer, 1) << "\n---\n")...;
    (std::cout << int... << ": " << @member_decl_strings(pointer, 2) << "\n---\n")...;
    (std::cout << int... << ": " << @member_decl_strings(pointer, 4) << "\n")...;
}
其中1表示只打印public成员,2表示只打印protected成员,4表示只打印private成员,所以输出为:
0float x
---
0float y
---
0float z
完整的权限数字是0-7,其中0表示什么都没有,3表示所有public和protected成员,5表示所有public和private成员,6表示所有protected和private成员,7表示所有成员。
第一章中,曾展示过标准静态反射转换枚举到字符串的实现,Circle亦可轻松实现。
enum class Days {
    Monday = 1,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
};

template<typename Enum>
requires std::is_enum_v<Enum>
std::string enum_to_string(Enum value) {
    switch(value) {
        @meta for enum(Enum e : Enum) {
            case e:
                return @enum_name(e);
        }

        default:
            return "<unnamed>";
    }
}

template<typename Enum>
void print_enumerations() {
    std::cout << @type_string(Enum) << ":\n";
    (std::cout << int... << ": " << enum_to_string(@enum_values(Enum)) << '\n')...;
}

int main() {
    //print_members<person>();
    print_enumerations<Days>();
}
运行将会输出所有枚举成员:
$ circle refl.cxx && ./refl
Days:
0: Monday
1: Tuesday
2: Wednesday
3: Thursday
4: Friday
5: Saturday
6: Sunday
其中遍历Enum主要用的是@meta for enum,@enum_name用于获取枚举的字符值。将其写在switch当中,则会在编译时自动为每个枚举成员展开相应的case语句。
最简单的方式是直接使用@enum_name输出,此时就无需编写enum_to_string函数:
template<typename Enum>
void print_enumerations() {
    std::cout << @type_string(Enum) << ":\n";
    (std::cout << int... << ": " << @enum_name(@enum_values(Enum)) << '\n')...;
}
只需要一行代码,是不是超级简洁呢!
相反,亦可从字符串来产生枚举类型,如下代码:
template<typename Enum>
std::optional<Enum> string_to_enum(const char* s) {
    @meta for enum(Enum e : Enum) {
        if(0 == strcmp(@enum_name(e), s))
            return e;
    }
    return { };
}

int main() {
    //print_members<person>();
    //print_enumerations<Days>();
    auto value = string_to_enum<Days>("Sunday");
    assert(*value == Days::Sunday);
}
可以看到,灵活性非常强。

自定义Attributes

反射和自定义Attributes组合起来,程序能够产生极大的灵活性。
若大家使用过Java的注解,就一定能够明白这东西的意义,Java的许多开源框架,如著名的Spring,底层便基于注解与反射。
Circle中自定义Attribute并不复杂,一个简单的例子:
// declare user attributes
using color [[attribute]] = const char*;
using country [[attribute]] = const char*;

struct book {
    [[.color="red"]] std::string title;
    [[.country="China"]] std::string author;
    int page_count;
};



int main() {
    // print_members<person>();
    // print_enumerations<Days>();
    // (std::cout << int... << ": " << @member_decl_strings(pointer, 1) << "\n---\n")...;
    // (std::cout << int... << ": " << @member_decl_strings(pointer, 2) << "\n---\n")...;
    // (std::cout << int... << ": " << @member_decl_strings(pointer, 4) << "\n")...;
    book b;
    std::cout << @attribute(b.@member_value(0), color);   // output: red
    std::cout << @attribute(b.@member_value(1), country); // output: China

}
自定义attribute有两种方式,语法如下:
using attrib-name [[attribute]] = attrib-type;
using attrib-name [[attribute]] = typename;
第一种是非类型attribute,可以指定值;第二种是类型attribute,可以指定类型。
例子中属于第一种方式,分别为color和country进行赋值,值类型为const char*。赋值之后,通过@attribute便能够访问所定义的attribute值。
枚举也支持自定义attributes,可以改写前面的代码如下:
using vacation [[attribute]] = const char*;

enum class Days {
    Monday = 1,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday [[.vacation="true"]],
    Sunday [[.vacation="true"]]
};

template<typename Enum>
requires std::is_enum_v<Enum>
std::string enum_to_string(Enum value) {
    switch(value) {
        @meta for enum(Enum e : Enum) {
            case e:
                if constexpr (@enum_has_attribute(e, vacation))
                    return @enum_attribute(e, vacation)
;
                else
                    return @enum_name(e);
        }

        default:
            return "<unnamed>";
    }
}

int main() {
    // print_members<person>();
    // print_enumerations<Days>();
    // (std::cout << int... << ": " << @member_decl_strings(pointer, 1) << "\n---\n")...;
    // (std::cout << int... << ": " << @member_decl_strings(pointer, 2) << "\n---\n")...;
    // (std::cout << int... << ": " << @member_decl_strings(pointer, 4) << "\n")...;
    // book b;
    // std::cout << @attribute(b.@member_value(0), color);   // output: red
    // std::cout << @attribute(b.@member_value(1), country); // output: China
    (std::cout << enum_to_string(@enum_values(Days)) << "\n---\n")...;

}
输出将为:
Monday
---
Tuesday
---
Wednesday
---
Thursday
---
Friday
---
true
---
true
---
有了这种自定义Attributes支持,就可以根据attributes的值来动态处理相应的成员。
比如对于一个服务器,为每种类型的消息函数添加attributes,便可以根据该attribute值自动实现逻辑分派。

Typed enums

读过《C++设计新思维》的应该知道,其中包含一个强大的工具TypeList,Circle中内建了这个类型,名为typed enums。
普通的enum是整型值,而typed enums中存放的是类型,不像Loki中的TypeList,此内建类型并非采用链表来连接每个类型,而是采用数组。
来看看其基本用法,代码如下:
enum typename type_list {
    int,
    double,
    char*,
    char,
    std::string
};

template<typename type_t>
void print_typed_enum() {
    std::cout << @type_string(type_t) << '\n';
    (std::cout << int... << ": " << @enum_type_strings(type_t) << '\n')...;
}
通过enum typename来定义一个typed enums,访问起来与普通enum一样。调用输出将为:
type_list
0int
1double
2char*
3char
4std::string
我们还可以通过反射来增加类型,扩展type_list,如下:
enum typename type_list_join {
    float;
    int[5];

    // 使用包展开定义类型
    @enum_types(type_list)...;

    // 逆序展开
    @enum_types(type_list)...[::-1] ...;
};
这个新的typed enums包含着两个自己的类型,其余类型通过包展开添加type_list中的类型。其中...[::-1]用于指定包展开的顺序为逆序展开,也可以指定特定区间的类型,完整语法是...[start:stop:step],分别表示开始位置、结束位置、步长,非常灵活。
故上述代码的输出将为:
type_list_join
0float
1int[5]
2int
3double
4char*
5char
6std::string
7std::string
8char
9char*
10double
11int
除此之外,还可以对typed enums应用算法,比如对type_list中的所有类型进行排序,
enum typename sorted_type_list {
    @meta std::array types {
        std::make_pair<std::stringint>(
            @enum_type_strings(type_list_join),
            int...
        )...
    };

    @meta std::sort(types.begin(), types.end());

    @enum_type(type_list_join, @pack_nontype(types).second) ...;
};
此处,先定义了一个std::array类型的types,元素类型为一个pair元素,第一个键为enum的名称,第二个键为名称所对应的索引。
之后,对types进行排序,根据排序结果,就能够得到排好序的索引。借助@pack_nontype可以将容器中的值表示为非类型参数包,因此可以进行展开索引。
因此输出将为:
sorted_type_list
0char
1char
2char*
3char*
4double
5double
6float
7int
8int
9int[5]
10std::string
11std::string
Loki中为TypeList编写的诸多算法,如索引式访问、唯一判断、存在查询等等,在Circle中基于反射这些都可以轻松实现。
若要遍历类型,除了前面的for-enum,Circle还提供了for-typename。可以遍历类型列表,亦可遍历typed enums,用法如下:
template<typename... types>
void print_types() {
    @meta for typename(type_t : { types... }) {
        std::cout << @type_string(type_t) << '\n';
    }
}

template<typename type_list>
void print_typed_enum() {
    // std::cout << @type_string(type_t) << '\n';
    // (std::cout << int... << ": " << @enum_type_strings(type_t) << '\n')...;
    @meta for typename(type_t : enum type_list)
        std::cout << @type_string(type_t) << '\n'
;
}

print_types<intcharfloatdoublestd::string>();
print_typed_enum<sorted_type_list>();
总而言之,typed enums配上反射非常好用,操纵类型和容器一样方便。

动态命名

若想动态定义变量名,当前只能根据宏来生成,但局限很大,能力甚微。
而有了反射,动态命名应该轻而易举,Circle便支持此特性,例如:
template<typename... type_t>
struct tuple_t {
    const char* @("var");
    type_t @(int...) ...;
};

// 遍历所有定义成员全称
@meta puts(@member_decl_strings(tuple_t<int, char, double>))...;
通过@()操作符可以连接字符串或数字来动态产生变量名,而指定pack index则会动态产生_0, _1...这样的名称。
因此,上述代码其实是为tuple_t动态定义了如下成员,
const char* var;
int _0;
char _1;
double _2;
这个特性,将为自动产生代码带来极大的活力。

动态生成类

基于Circle的这种强大反射能力,可以真正实现「自动生成类」。
比如将类型信息保存成JSON数据,那么根据这些数据,依赖反射就可以动态生成类型。
让我们先准备JSON格式的类型数据,代码如下:
@meta json j;
@meta j["types"]["name"] = "person";
@meta j["types"]["members"] = { {{"name", "age"}, {"type", "int"}},
              {{"name", "name"},{"type", "std::string"}} };
这里借助了nlohmann json库,由于是静态反射,因此数据也必须在编译期产生,所以前面都加上了@meta。以上定义的数据为,
{
    "types": {
        "members": [
            {
                "name""age",
                "type""int"
            },
            {
                "name""name",
                "type""std::string"
            }
        ],
        "name""person"
    }
}
就是前面的person类,不过如今是通过JSON和反射来动态定义该类。
然后,通过反射来动态生成类,代码如下:
@meta json& types = j["types"];
@meta struct @(types["name"]) {
    @meta for(json& members : types["members"]) {
        @type_id(members["type"]) @(members["name"]);
    }
};

enum typename type_list {
    @type_id(types["name"]);
};


int main() {

    @meta for typename(type_t : enum type_list) {
        std::cout << "struct " << @type_string(type_t) << "{\n";
        (std::cout << @member_decl_strings(type_t) << ";\n")...;
        std::cout << "};\n";
    }


}
通过动态命名操作符@()可以根据字符产生类名,再通过@type_id定义类型,便可完整地定义类型的所有成员。
为了访问类型信息,将其置入typed enums,接着就能够使用for-typename进行打印类型信息。因此输出如图:
这种层次的反射能力非常强大,像ORM、远程调用等等需求实现起来轻而易举。

总结

Circle的反射扩展,是目前除标准之外能力最强的。
正规项目中肯定无法使用这些扩展,但是可以一睹反射所应该具备的能力。
事实上,C++反射一直在铺路,像是依次增加的编译期关键字,就是在为静态反射做准备。标准中将加入的反射,只会比Circle扩展的反射能力更强,其中的有些概念Circle当中也没有,支持的功能更多,只是还须不少时间。
只能说,标准现在所支持的反射预备特性太少了,即便是reflection ts,使用起来跟Circle的体验都相差甚远。
故想用上标准反射,C++26可能都是早的了:P

- EOF -


加主页君微信,不仅C/C++技能+1

主页君日常还会在个人微信分享C/C++开发学习资源技术文章精选,不定期分享一些有意思的活动岗位内推以及如何用技术做业余项目

加个微信,打开一扇窗


推荐阅读  点击标题可跳转

1、C++ 反射:探索

2、C++ 反射:全面解读 property 的实现机制!

3、C++ 反射:深入浅出剖析 ponder 库实现机制!


关注『CPP开发者』

看精选C/C++技术文章 

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存